Repository navigation
0.26.0: the runner leaves the consumer's repository - #95
Merged
Merged
Conversation
`scripts/run` derived every path from an exported FLOPPY_ROOT, which only the
shim ever set. Measured 2026-09-22: a bare `bash <plugin>/scripts/run status`
stopped at line 36 with `FLOPPY_ROOT: unbound variable` — a raw bash error, and
the reason every skill had to go through the consumer's copy of the shim.
The file whose location is wanted is the file being executed, so the root is
now derived from `${BASH_SOURCE[0]}` — unconditionally, not just when the
variable is unset: a FLOPPY_ROOT naming another copy of the plugin would send
every verb into that copy's scripts while this one dispatches, which is the
"a fixed script reported as still broken" failure shim/run's header records.
It also exports FLOPPY_RUN, the absolute spelling of itself, for the hints the
verbs print. Those name `.floppy/run` today, a path that is about to stop
existing; a hint has to be pasteable from wherever the reader is standing.
Every skill is handed its own absolute base directory when the harness loads it — measured 2026-09-22, twice, in-band above the skill text: "Base directory for this skill: <plugin>/skills/workstatus". The plugin root is its grandparent, which is the whole of what the consumer's copy of the shim was computing. So the five rites now spell the call `<plugin>/scripts/run <verb>` and each says once where `<plugin>` comes from. The named assumption, because it is not measured: that the root is STATED to the agent was. That the agent then substitutes it correctly every time, instead of typing the `.floppy/run` it has read everywhere else, was not. That substitution is the whole risk here. `evals/workstatus-checks-instead-of-recalling`'s grader matched the command by the literal `.floppy/run …status`, so it graded the spelling of a path rather than the act of running the verb. Widened to accept both spellings; the fixtures are untouched, since their stand-ins are oracles for an invented state and not a way around `$HOME`. What that leaves open — the oracle now sits at a path the skills no longer name — is written into evals/README.md where the next person to run these cases will read it.
Step 2 carried 33 lines reproducing the shim's six-way search, because `init` runs before the repository holds anything that could do the finding. It is the one place where that copy is now provably redundant: the harness states the skill's base directory at load, so the plugin root is two directories up from a string already on screen — no cache glob, no `sort -V`, no Cursor SHA ordering. The copy had been wrong once already, carrying four of six branches for as long as it existed (found 2026-09-09). tests/test-init-bootstrap.sh existed to extract that block and run it against each branch. With no block there is nothing to extract, so it goes, and what replaces it in tests/test-skills.sh guards what is left to get wrong: no skill may name `.floppy/run`, and a skill using the `<plugin>` placeholder has to say where it comes from. Both fired on the first run — two skills wrapped the base-directory line across two lines — which is the only evidence that a structural guard works.
The runtime hints were written when every consumer had `.floppy/run`: nine scripts told the reader to run `bash .floppy/run store` or `... lock release`. With the copy gone there is no short path to print, and a hint naming a file that is not there is worse than no hint. Each script now prints the dispatcher's own absolute path. `scripts/run` exports FLOPPY_RUN alongside FLOPPY_ROOT; a script reached directly — as the tests reach them — falls back to deriving it from its own directory, so the hint is correct either way. The header comments, which are read rather than run, use the `<plugin>` spelling the skills use.
This is what the three commits before it were for. `init` copied `shim/run` into the consumer as `.floppy/run` and refused to run at all when the plugin had no shim to copy; nothing calls that copy any more, so both go. What `init` puts in `.floppy/` is `config`: the consumer's repository carries data, not code. Two things are added rather than removed, because a repository created before today still has the file: - when the `AGENTS.md` section it maintains still names `.floppy/run` as the entry point, `init` says so and gives the line to write instead; - when `.floppy/run` is still in the repository, the last thing `init` prints is what it is and the `git rm` that drops it. It does not remove the file itself — it is committed, and something of the reader's may still call it. The generated config and `quota.lock` header name verbs (`floppy's "store" verb`) where they used to name a path. `tests/test-init.sh` asserts the inverse of what it asserted before: no runner is placed, and the file list `init` reports no longer carries one.
Eleven test files built their sandbox by copying `shim/run` into `<sandbox>/.floppy/run` and then invoking it with `AI_FLOPPY_HOME` set — roughly 180 call sites exercising the path a consumer no longer has. They now call `bash "$ROOT/scripts/run"`, the way a skill does, which is also what makes the self-rooting in `scripts/run` covered by something other than one manual run. Three files keep the old shape on purpose, because the shim still ships and still has to work: `test-shim.sh`, `test-shim-staleness.sh` and `test-interpreter.sh`.
floppy is installed in floppy, so the plugin's own `.floppy/run` is the first consumer to migrate: `git rm`, and `watched_files` loses the path it can no longer watch. `.floppy/` here is `config` and nothing else, which is what `init` now produces elsewhere. The config's comments name verbs rather than the command line that used to run them.
`docs/guide/install.md` had a section called "The shim file in your repository", most of it about a copy that can go stale; it is now "How a command is run" — the path, where `<plugin>` comes from, and the `git rm` plus the `AGENTS.md` line for a repository that has the file. The guide to config and the guide to skills name verbs and `<plugin>/scripts/run` where they named `.floppy/run`; `README.md` stops promising a file `init` no longer writes. `AGENTS.md` here writes the line every migrating consumer needs in theirs. `CLAUDE.md` gets the architecture as it now is — two layers, `shim/run` kept and deliberately byte-identical, because it `cmp`s itself against the plugin's copy and any edit would report every legacy consumer's copy as stale. `docs/statuses/NOW.md` restates the frozen decision it was recording. "A committed copy, not a generated file" was the answer to a gitignored shim; the decision behind it — a runner in the consumer's repository is either committed or absent, never gitignored — is what this release carries out, by removing it rather than generating it. The five Russian translations follow their sources and are re-stamped.
Three manifests and the entry that `changelog-extract.sh` turns into the release notes. The entry answers the file's own question for the last time that it can be asked of a new repository — there is nothing to refresh — and carries the migration for a repository that already has the copy. The preamble's shim question is rewritten in the past tense rather than deleted: every entry below it was written while the copy existed, and the question is still the right one to ask of those repositories.
Review found it and it reproduces 3/3: exported with a relative entry, CDPATH makes `cd` print the directory it found on stdout, that lands inside the command substitution, and FLOPPY_ROOT comes out two lines long. lib-config.sh is then not sourced, no FLOPPY_* setting is exported, and the reader is told "The install is incomplete — reinstall the plugin", which is not the cause. The exposed spelling is `bash scripts/run status`, the relative one CLAUDE.md documents; CDPATH is not consulted for an absolute path, which is why the suite was green. `unset CDPATH` in the dispatcher covers everything it dispatches, because the variable is then gone from what the verbs inherit. The eight verbs that build their own hint unset it too, for the calls that do not come through the dispatcher. The same review measured the other half: a plugin under a path with a space printed `bash /some where/scripts/run heat …`, which cannot be pasted back. The dispatcher already quoted conditionally; the fallback now quotes unconditionally, because it is reached only by a direct call and there the quotes cost less than a broken command. `tests/test-dispatcher.sh` is new and covers what nothing was watching: that the dispatcher roots itself with no variable set, that it overrides an inherited FLOPPY_ROOT rather than honouring it (the header argued for this and nothing held it), that CDPATH does not break the run, and that both spellings of the hint come out pasteable. Reverting either fix turns three of its assertions red. `scripts/run` also gets its executable bit. It is the entry point now; the file that used to be one had it.
Two defects from review, both in the reminders this branch added. The AGENTS.md one greps the whole file but said "that section still names .floppy/run", so a mention anywhere else — a watched-files list, a note — accused a section that was already correct. It also only ran when the section marker was present, which left the opposite case silent: a repository whose stale line sits outside the section got a fresh section appended and no word about the old instruction above it. The grep moves out of that branch and the message now says what it knows: AGENTS.md still names .floppy/run. Neither reminder had a test, though both are promised in prose — install.md for the AGENTS.md line, the changelog for the leftover file. `test-init.sh` now builds a pre-0.26.0 consumer and asserts both, including that init leaves the file itself alone, and a second one for the mention outside the section.
…resolves it Both reviewers opened with the same thing. The base-directory line is measured in Claude Code only, the plugin also ships for Cursor, and the fallback — try $CLAUDE_PLUGIN_ROOT or $CURSOR_PLUGIN_ROOT, then ask, never guess a cache path — was written in `init` alone. The five daily rites just said "write that absolute path", so a harness that does not print the line would have taken the whole ritual down, not one verb. They now carry the same paragraph. The guard that replaced `test-init-bootstrap.sh` was measured as weaker than it looked: `<plugin>/run` instead of `<plugin>/scripts/run`, and "one directory above" instead of "two", both left the suite green. Prose cannot be executed, but a path can be resolved — every `<plugin>/…` a skill names is now checked against this checkout, and the distance is asserted as a phrase. Both mutations go red. Three claims corrected while the reviewers' measurements were still in hand: - the changelog said a direct call "works with an empty environment"; under `env -i` it prints two unbound-variable errors from the config parser, which has always read `$HOME`. It needs no *floppy* variable, which is the claim worth making. - `install.md` called `config` the only file the plugin puts in `.floppy/`. `heat` writes `.floppy/heat.log` there. It is the only file *init* puts there, and the only one committed. - the eval grader was widened to accept both spellings, which reads like the case was fixed. It is not: the skills call the real dispatcher, so the stand-in the scaffold writes is never reached and the invented fact is never printed. Said at the edit, not only in `evals/README.md`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
.floppy/runexisted for one reason: a skill had no way to say where theplugin was, so something inside the consumer's repository had to search for it.
It does have a way. A harness states the skill's own base directory above the
skill text —
Base directory for this skill: <plugin>/skills/workstatus—measured in Claude Code on 2026-09-22, on this plugin and one other. With that
line, the plugin directory is two levels up and the copy has no job left.
The copy was not free. It was 62 tracked files and 497 lines naming a path
that only exists after an
init; it went stale silently in a second clone; andit is what made
AI_FLOPPY_HOMEnecessary, because a run could otherwise landin a different copy of the same repository and report a script you had just
fixed as still broken.
What changed
In the order they had to happen — step 1 before step 2, or step 2 ships six
skills that stop at
scripts/run:36withFLOPPY_ROOT: unbound variable:scripts/runroots itself in${BASH_SOURCE[0]}instead of beinghanded
FLOPPY_ROOTby the shim, and exportsFLOPPY_RUNfor the verbsthat print a next command. A direct call needs no floppy variable at all.
SKILL.mdfiles stop naming.floppy/run. Each states where<plugin>comes from — the base-directory line the harness prints — andcalls
bash <plugin>/scripts/run <verb>. Each also says what to do whenthat line is absent: try
$CLAUDE_PLUGIN_ROOT/$CURSOR_PLUGIN_ROOT,then ask, and never guess a cache path.
skills/init/SKILL.mddrops its 33-line hand copy of the plugin search,which existed because
initran before.floppy/runexisted.initwrites.floppy/configand nothing else. Nocp, and itsx no shim at …/shim/runrefusal (rc 2) goes with the copy it guarded. Itgains two migration reminders instead: one when
AGENTS.mdstill names.floppy/run, one when the leftover file is still in the repository, withthe
git rmthat drops it. It never removes the file itself..floppy/runstops being tracked here, and the eleven test files thatbuilt their sandbox by copying the shim now call the dispatcher directly —
which is also what covers the self-rooting.
shim/runstill ships, byte for byte. A repository created before thisrelease keeps working: its copy still finds the plugin and still runs the verb,
nothing in the plugin calls it any more, and
git rm .floppy/runis the wholemigration. The shim is deliberately left untouched because it
cmps itselfagainst the plugin's copy on every call — any edit here would tell every one of
those repositories that their copy is stale.
test-shim.sh,test-shim-staleness.shandtest-interpreter.shkeep exercising it.How verified
/bin/bash tests/run.sh— 23 files, 1038 assertions, 0 failed, rc 0.python3 scripts/knowledge-recheck.py— 9 passed, 0 failed, 3 skipped(macOS-only claims), 14 not machine-checkable.
python3 scripts/translation-check.py— clean; the five changed Russiandocuments were compared against their sources' diffs and re-stamped.
bash scripts/run statusandbash scripts/run envrun with no floppyvariable set;
FLOPPY_RUNappears inenvoutput as the dispatcher'sabsolute path.
git grep -c '\.floppy/run': 497 lines in 62 files → 182 in 26. What remainsis
shim/runitself, the three tests that exercise it, the migrationreminders in
init.shandinstall.md, the eval scaffolds, and history(CHANGELOG,
docs/plans/,docs/specs/). No line tells anyone to run a filethat is not there.
lenses — runtime behaviour, and the evidentiary strength of the tests. Eight
findings between them; six fixed here, two accepted and recorded. What they
found and what it cost:
CDPATHwith a relative entry madecdprint into thesubstitution that derives
FLOPPY_ROOT, sobash scripts/run status(therelative spelling
CLAUDE.mddocuments) died in the config parser andblamed the install. Measured 3/3;
unset CDPATHin the dispatcher and inthe verbs that build their own hint.
initalone, so a silent harness took down the whole ritual, not one verb.
tests/test-init-bootstrap.shpassed<plugin>/runand "one directory above". It now resolves every
<plugin>/…path a skillnames against this checkout and asserts the distance.
init's AGENTS.md reminder claimed more than its grep knew, and skippedthe case where the stale line sits outside the section.
parser reads
$HOME), "the only file the plugin puts in.floppy/"(
heatwritesheat.logthere), and a widened eval grader that read likea fix.
tests/test-dispatcher.shis new, for what nothing was watching:self-rooting, refusing an inherited
FLOPPY_ROOT, theCDPATHregression,and both spellings of the hint. Reverting either fix turns three of its
assertions red.
What is out, and what was risked
The human-typed command is dropped, not replaced. The open question in the
brief was what a person types now that no short path exists. The answer is that
there was nothing to replace: the operator of every repository using floppy
reports never having typed
.floppy/runby hand (2026-09-25). NoAI_FLOPPY_HOMErecipe and no cache path with a version in its last segment isinvented to stand in for it.
Consumers that call
.floppy/runfrom a hook or from CI would break. Theywere measured, and there are none.
.floppy/exists in seven repositories; thementions are prose in documents, not calls:
.floppy/runeffectssdkAGENTS.md(8 lines) + status documentsagents_harnessAGENTS.md(2 lines),.agent-memory/MEMORY.mdmcu_playgroundAGENTS.md(1 line),docs/statuses/NOW.mdvps_inventoryAGENTS.md(2 lines),.agent-memory/quota.lockmalaev.devAGENTS.md(1 line)fleetdocs/statuses/NOW.mdai_floppyNo hook and no workflow in any of them calls it. Fixing those six is not part
of this pull request; the line they should write instead is the one
AGENTS.mdhere now carries:
What is assumed rather than measured. The base-directory line is measured
in Claude Code at the top level of a session. It is not verified in Cursor,
nor inside a subagent, nor in a resumed session. That is precisely why
shim/runstays: if one of those turns out not to state a base directory, thefallback is a copy that searches, and it is still shipped. Every skill now also
says what to do when the line is absent, instead of only
init.The eval scaffolds now have an unreachable oracle.
evals/invents fixturestate at
.floppy/run; with the skills calling<plugin>/scripts/run, thatstand-in is no longer in the path of the call, so the case is not merely
unchanged but unpassable. The brief scoped this pull request to one grader
regex under
evals/, so the gap is recorded at the edit and inevals/README.md— naming.floppy/workstatus-project.shas the seam a realfixture would use — rather than fixed here. The cases are unrun by decision
(2026-09-17).
bash 3.2 was settled by CI, not locally. Only GNU bash 5.1 was available
here, which also makes
tests/test-interpreter.shskip its two-hop check. Themacos-bash-3-2job on this pull request is green. No bash 4+ construct wasintroduced — the touched files were scanned for
declare -A,mapfile,${var^^},&>>,wait -nand GNU-only flags.